跳到主要内容

C# 事件与委托

C# 中的委托

参考资料 委托(C# 编程指南)

要学习 C# 这个委托 delegate 就是一个绕不开的关键字

public delegate int PerformCalculation(int x, int y);

委托类似于 C++ 函数指针,但委托完全面向对象,不像 C++ 指针会记住函数,委托会同时封装对象实例和方法。

实例化委托(Delegate)

一旦声明了委托类型,委托对象必须使用 new 关键字来创建,且与一个特定的方法有关。当创建委托时,传递到 new 语句的参数就像方法调用一样书写,但是不带有参数。

例如:

public delegate void printString(string s);
...
printString ps1 = new printString(WriteToScreen);
printString ps2 = new printString(WriteToFile);

下面的实例演示了委托的声明、实例化和使用,该委托可用于引用带有一个整型参数的方法,并返回一个整型值。

using System;

// 定义一个委托
delegate int NumberChanger(int n);

namespace DelegateAppl
{
class TestDelegate
{
static int num = 10;
public static int AddNum(int p)
{
num += p;
return num;
}

public static int MultNum(int q)
{
num *= q;
return num;
}
public static int getNum()
{
return num;
}

static void Main(string[] args)
{
// 创建委托实例(传入的就是方法引用)
NumberChanger nc1 = new NumberChanger(AddNum);
NumberChanger nc2 = new NumberChanger(MultNum);

// 使用委托对象调用方法
nc1(25);
Console.WriteLine("Value of Num: {0}", getNum());
nc2(5);
Console.WriteLine("Value of Num: {0}", getNum());
Console.ReadKey();
}
}
}

委托的多播

委托对象可使用 "+" 运算符进行合并。一个合并委托调用它所合并的两个委托。只有相同类型的委托可被合并。"-" 运算符可用于从合并的委托中移除组件委托。

使用委托的这个有用的特点,您可以创建一个委托被调用时要调用的方法的调用列表。这被称为委托的 多播(multicasting),也叫组播。下面的程序演示了委托的多播:

using System;

delegate int NumberChanger(int n);
namespace DelegateAppl
{
class TestDelegate
{
static int num = 10;
public static int AddNum(int p)
{
num += p;
return num;
}

public static int MultNum(int q)
{
num *= q;
return num;
}
public static int getNum()
{
return num;
}

static void Main(string[] args)
{
// 创建委托实例
NumberChanger nc;
NumberChanger nc1 = new NumberChanger(AddNum);
NumberChanger nc2 = new NumberChanger(MultNum);
nc = nc1;
nc += nc2;
// 调用多播
nc(5);
Console.WriteLine("Value of Num: {0}", getNum());
Console.ReadKey();
}
}
}

输出为:

Value of Num: 75

委托的用途

实际上这个可以用来做回调函数

using System;
using System.IO;

namespace DelegateAppl
{
class PrintString
{
static FileStream fs;
static StreamWriter sw;
// 委托声明
public delegate void printString(string s);

// 该方法打印到控制台
public static void WriteToScreen(string str)
{
Console.WriteLine("The String is: {0}", str);
}
// 该方法打印到文件
public static void WriteToFile(string s)
{
fs = new FileStream("c:\\message.txt", FileMode.Append, FileAccess.Write);
sw = new StreamWriter(fs);
sw.WriteLine(s);
sw.Flush();
sw.Close();
fs.Close();
}
// 该方法把委托作为参数,并使用它调用方法
public static void sendString(printString ps)
{
ps("Hello World");
}
static void Main(string[] args)
{
printString ps1 = new printString(WriteToScreen);
printString ps2 = new printString(WriteToFile);
sendString(ps1);
sendString(ps2);
Console.ReadKey();
}
}
}

C# 事件(Event)

事件(Event) 基本上说是一个用户操作,如按键、点击、鼠标移动等等,或者是一些提示信息,如系统生成的通知。应用程序需要在事件发生时响应事件。

例如,中断。

而且 C# 中可以使用事件机制实现线程间的通信。

通过事件使用委托

事件在类中声明且生成,且通过使用同一个类或其他类中的委托与事件处理程序关联。

  • 包含事件的类用于发布事件。这被称为 发布器(publisher) 类
  • 其他接受该事件的类被称为 订阅器(subscriber) 类

事件使用 发布-订阅(publisher-subscriber) 模型。

发布器(publisher) 是一个包含事件和委托定义的对象。事件和委托之间的联系也定义在这个对象中。发布器(publisher)类的对象调用这个事件,并通知其他的对象。

订阅器(subscriber) 是一个接受事件并提供事件处理程序的对象。在发布器(publisher)类中的委托调用订阅器(subscriber)类中的方法(事件处理程序)。

声明事件(Event)

在类的内部声明事件,首先必须声明该事件的委托类型。例如:

public delegate void BoilerLogHandler(string status);

然后,声明事件本身,使用 event 关键字:

// 基于上面的委托定义事件
public event BoilerLogHandler BoilerEventLog;

上面的代码定义了一个名为 BoilerLogHandler 的委托和一个名为 BoilerEventLog 的事件,该事件在生成的时候会调用委托。

使用实例

using System;
namespace SimpleEvent
{
using System;
/***********发布器类***********/
public class EventTest
{
private int value;

public delegate void NumManipulationHandler();
public event NumManipulationHandler ChangeNum;

protected virtual void OnNumChanged()
{
if ( ChangeNum != null )
{
ChangeNum(); /* 事件被触发 */
}else {
Console.WriteLine( "event not fire" );
Console.ReadKey(); /* 回车继续 */
}
}


public EventTest()
{
int n = 5;
SetValue( n );
}


public void SetValue( int n )
{
if ( value != n )
{
value = n;
OnNumChanged();
}
}
}


/***********订阅器类***********/

public class subscribEvent
{
public void printf()
{
Console.WriteLine( "event fire" );
Console.ReadKey(); /* 回车继续 */
}
}

/***********触发***********/
public class MainClass
{
public static void Main()
{
EventTest e = new EventTest(); /* 实例化对象,第一次没有触发事件 */
subscribEvent v = new subscribEvent(); /* 实例化对象 */

e.ChangeNum += new EventTest.NumManipulationHandler( v.printf ); /* 注册 */

e.SetValue( 7 );
e.SetValue( 11 );
}
}
}

当上面的代码被编译和执行时,它会产生下列结果:

event not fire
event fire
event fire

什么场景得使用它们

请看下面的场景:首领A 要搞一场鸿门宴,吩咐部下 B 和 C 各自带队埋伏在屏风两侧,约定以杯为令:若左手举杯,则 B 带队杀出;若右手举杯,则 C 带队杀出;若直接摔杯,则 B 和 C 同时杀出。B 和 C 袭击的具体方法,首领 A 并不关心

有脑子反应快的同学,马上说,这个逻辑,用一个 IF 条件判断语句不就搞定了吗:

if(左手举杯)
{
B带队杀出;
}
else if(右手举杯)
{
C带队杀出;
}
else if(直接摔杯)
{
B带队杀出;
C带队杀出;
}
else
{
按兵不动
}

但是问题来了首领A会什么时候发出信号呢?

估计连他自己都不知道。那么上面这段伪码,要插入到哪里呢?难道 B 和 C 就一直不停的用 While 循环做上述判断吗?显然不合适吧。更重要的,如果扩展一下,部下不止 B 和 C,而是 B、C、D、E 等等,每个人都有自己不同的响应领导号召的方法,那么对首领 A 而言,要分别去跟每个部下打交道,上述的伪码,会进行无尽的扩展和完善,显然也不是我们所希望的。

正确的逻辑应该是,B 和 C 不管宴席上发生的任何其他事情,只等首领发出举杯或者摔杯的信号,一旦首领 A 发出信号,相当于通知了所有部下,凡是约定好的部下,都立马各自行动!

我们定义三个类,分别模拟首领 A 和部下 B、C:

首领 A 的类有举杯和摔杯两种方法,其中,举杯方法带有一个形参,用于传递是左手举杯,还是右手举杯。部下 B 和 C 的类,各自有一种攻击方法。

/// <summary>
/// 首领A
/// </summary>
public class A
{
/// <summary>
/// 举杯
/// </summary>
/// <param name="hand">手:左、右</param>
public void Raise(string hand)
{
Console.WriteLine("首领A{0}手举杯", hand);
}

/// <summary>
/// 摔杯
/// </summary>
public void Fall()
{
Console.WriteLine("首领A摔杯");
}
}


/// <summary>
/// 部下B
/// </summary>
public class B
{
/// <summary>
/// 攻击
/// </summary>
public void Attack()
{
Console.WriteLine("部下B发起攻击");
}
}


/// <summary>
/// 部下C
/// </summary>
public class C
{
/// <summary>
/// 攻击
/// </summary>
public void Attack()
{
Console.WriteLine("部下C发起攻击");
}
}

至此,三个独立的类,构造完毕。那么,怎么让部下 B 和 C,根据首领的暗语才去相应的行动呢?

我们需要在首领 A 类中,采用一种方法,把其意图传递出去。在首领 A 类之前,分别定义一个带形参的举杯委托 RaiseEventHandler 和一个不带形参的摔杯委托 FallEventHandler。

命名规则是在准备传递的方法名后加上 EventHandler(命名规则,增加可读性)

delegate void RaiseEventHandler(string hand);
delegate void FallEventHandler();

然后,在首领 A 类中,定义两个事件。

这个类似于大家在 WinForm 中拖放一个 Button 后,双击该 Button,即可编辑其事件。

/// <summary>
/// 首领A举杯事件
/// </summary>
public event RaiseEventHandler RaiseEvent;

/// <summary>
/// 首领A摔杯事件
/// </summary>
public event FallEventHandler FallEvent;

好了,做完上面两个步骤后,就可以在首领 A 的举杯和摔杯方法中,调用上述两个事件了。这样,如果 B 和 C 中订阅该事件,便可自动执行了。

/// <summary>
/// 举杯
/// </summary>
/// <param name="hand">手:左、右</param>
public void Raise(string hand)
{
Console.WriteLine("首领A{0}手举杯", hand);
// 调用举杯事件,传入左或右手作为参数
if (RaiseEvent!=null)
{
RaiseEvent(hand);
}
}

/// <summary>
/// 摔杯
/// </summary>
public void Fall()
{
Console.WriteLine("首领A摔杯");
// 调用摔杯事件
if (FallEvent!=null)
{
FallEvent();
}
}

在部下 B 和 C 心中,必须存在首领 A,才能执行 A 的暗示吧。所以,在 B 类和 C 类中,需要声明一个 A,该声明可以通过 B 和 C 的构造函数进行实例化。实例化之后,便可在类 B 和类 C 中订阅类 A 的事件了。

完整代码如下:

class Program
{
static void Main(string[] args)
{
A a = new A(); // 定义首领A
B b = new B(a); // 定义部下B
C c = new C(a); // 定义部下C
// 首领A左手举杯
a.Raise("左");
// 首领A右手举杯
//a.Raise("右");
// 首领A摔杯
//a.Fall();
Console.ReadLine();
// 由于B和C订阅了A的事件,所以无需任何代码,B和C均会按照约定进行动作。
}
}

/// <summary>
/// 首领A举杯委托
/// </summary>
/// <param name="hand">手:左、右</param>
public delegate void RaiseEventHandler(string hand);


/// <summary>
/// 首领A摔杯委托
/// </summary>
public delegate void FallEventHandler();


/// <summary>
/// 首领A
/// </summary>
public class A
{
/// <summary>
/// 首领A举杯事件
/// </summary>
public event RaiseEventHandler RaiseEvent;

/// <summary>
/// 首领A摔杯事件
/// </summary>
public event FallEventHandler FallEvent;

/// <summary>
/// 举杯
/// </summary>
/// <param name="hand">手:左、右</param>
public void Raise(string hand)
{
Console.WriteLine("首领A{0}手举杯", hand);
// 调用举杯事件,传入左或右手作为参数
if (RaiseEvent!=null)
{
RaiseEvent(hand);
}
}
/// <summary>
/// 摔杯
/// </summary>
public void Fall()
{
Console.WriteLine("首领A摔杯");
// 调用摔杯事件
if (FallEvent!=null)
{
FallEvent();
}
}
}


/// <summary>
/// 部下B
/// </summary>
public class B
{
A a;
public B(A a)
{
this.a = a;
a.RaiseEvent += new RaiseEventHandler(a_RaiseEvent); // 订阅举杯事件
a.FallEvent += new FallEventHandler(a_FallEvent); // 订阅摔杯事件
}
/// <summary>
/// 首领举杯时的动作
/// </summary>
/// <param name="hand">若首领A左手举杯,则B攻击</param>
void a_RaiseEvent(string hand)
{
if (hand.Equals("左"))
{
Attack();
}
}
/// <summary>
/// 首领摔杯时的动作
/// </summary>
void a_FallEvent()
{
Attack();
}
/// <summary>
/// 攻击
/// </summary>
public void Attack()
{
Console.WriteLine("部下B发起攻击,大喊:猛人张飞来也!");
}
}



/// <summary>
/// 部下C
/// </summary>
public class C
{
A a;
public C(A a)
{
this.a = a;
a.RaiseEvent += new RaiseEventHandler(a_RaiseEvent); // 订阅举杯事件
a.FallEvent += new FallEventHandler(a_FallEvent); // 订阅摔杯事件
}

/// <summary>
/// 首领举杯时的动作
/// </summary>
/// <param name="hand">若首领A右手举杯,则攻击</param>
void a_RaiseEvent(string hand)
{
if (hand.Equals("右"))
{
Attack();
}
}

/// <summary>
/// 首领摔杯时的动作
/// </summary>
void a_FallEvent()
{
Attack();
}

/// <summary>
/// 攻击
/// </summary>
public void Attack()
{
Console.WriteLine("部下C发起攻击,一套落英神掌打得虎虎生威~");
}
}

Reference

参考资料 C#委托与事件应用场景